package com.dtflys.easyel.runtime;

import com.dtflys.easyel.antlr.EasyElParser;
import com.dtflys.easyel.ast.ASTAccessExpression;
import com.dtflys.easyel.ast.ASTBinaryExpression;
import com.dtflys.easyel.ast.ASTConstant;
import com.dtflys.easyel.ast.ASTDate;
import com.dtflys.easyel.ast.ASTExpression;
import com.dtflys.easyel.ast.ASTIdentifier;
import com.dtflys.easyel.ast.ASTIndexExpression;
import com.dtflys.easyel.ast.ASTInvokeExpression;
import com.dtflys.easyel.ast.ASTList;
import com.dtflys.easyel.ast.ASTListGenerator;
import com.dtflys.easyel.ast.ASTMap;
import com.dtflys.easyel.ast.ASTMapEntry;
import com.dtflys.easyel.ast.ASTNegativeExpression;
import com.dtflys.easyel.ast.ASTNewInstance;
import com.dtflys.easyel.ast.ASTNotExpression;
import com.dtflys.easyel.ast.ASTNull;
import com.dtflys.easyel.ast.ASTRange;
import com.dtflys.easyel.ast.ASTTernaryExpression;
import com.dtflys.easyel.ast.ASTTimeDuration;
import com.dtflys.easyel.ast.ASTType;
import com.dtflys.easyel.ast.ASTVisitor;
import com.dtflys.easyel.runtime.eval.AccessEvaluator;
import com.dtflys.easyel.runtime.eval.AddEvaluator;
import com.dtflys.easyel.runtime.eval.BooleanEvaluator;
import com.dtflys.easyel.runtime.eval.CompareEvaluator;
import com.dtflys.easyel.runtime.eval.ConstantEvaluator;
import com.dtflys.easyel.runtime.eval.DivideEvaluator;
import com.dtflys.easyel.runtime.eval.IdentifierEvaluator;
import com.dtflys.easyel.runtime.eval.InEvaluator;
import com.dtflys.easyel.runtime.eval.IndexEvaluator;
import com.dtflys.easyel.runtime.eval.InvokeEvaluator;
import com.dtflys.easyel.runtime.eval.MultiplyEvaluator;
import com.dtflys.easyel.runtime.eval.NewInstanceEvaluator;
import com.dtflys.easyel.runtime.eval.NotEvaluator;
import com.dtflys.easyel.runtime.eval.PowerEvaluator;
import com.dtflys.easyel.runtime.eval.RangeEvaluator;
import com.dtflys.easyel.runtime.eval.RegexMatchEvaluator;
import com.dtflys.easyel.runtime.eval.RemainderEvaluator;
import com.dtflys.easyel.runtime.eval.SubEvaluator;
import com.dtflys.easyel.utils.AccessUtils;
import com.dtflys.easyel.utils.MetaHelper;
import com.dtflys.easyel.exception.EasyElAccessException;
import com.dtflys.easyel.exception.EasyElEvalException;
import com.dtflys.easyel.runtime.wrapper.NumberWrapper;
import org.antlr.v4.runtime.Token;

import java.lang.reflect.InvocationTargetException;
import java.util.*;

/**
 * @author gongjun[jun.gong@thebeastshop.com]
 * @since v1.0.0
 */
public class EasyElEvalVisitor implements ASTVisitor {

    private final ConstantEvaluator constantEvaluator = new ConstantEvaluator();
    private final IdentifierEvaluator identifierEvaluator = new IdentifierEvaluator();
    private final NewInstanceEvaluator newInstanceEvaluator = new NewInstanceEvaluator();
    private final NotEvaluator notEvaluator = new NotEvaluator();
    private final RangeEvaluator rangeEvaluator = new RangeEvaluator();
    private final AddEvaluator addEvaluator = new AddEvaluator();
    private final SubEvaluator subEvaluator = new SubEvaluator();
    private final MultiplyEvaluator multiplyEvaluator = new MultiplyEvaluator();
    private final DivideEvaluator divideEvaluator = new DivideEvaluator();
    private final RemainderEvaluator remainderEvaluator = new RemainderEvaluator();
    private final PowerEvaluator powerEvaluator = new PowerEvaluator();
    private final CompareEvaluator compareEvaluator = new CompareEvaluator();
    private final RegexMatchEvaluator regexMatchEvaluator = new RegexMatchEvaluator();
    private final BooleanEvaluator booleanEvaluator = new BooleanEvaluator();
    private final InEvaluator inEvaluator = new InEvaluator();
    private final AccessEvaluator accessEvaluator = new AccessEvaluator();
    private final IndexEvaluator indexEvaluator = new IndexEvaluator();
    private final InvokeEvaluator invokeEvaluator = new InvokeEvaluator();


    @Override
    public void visitType(EasyElRuntimeContext context, ASTType node) {

    }

    @Override
    public void visitConstant(EasyElRuntimeContext context, ASTConstant node) {
        Object result = constantEvaluator.evaluate(context, node);
        context.setResult(result);
    }

    @Override
    public void visitRange(EasyElRuntimeContext context, ASTRange node) {
        Comparable from = null, to = null;
        ASTExpression fromExpr = node.getFrom();
        ASTExpression toExpr = node.getTo();
        if (fromExpr != null) {
            fromExpr.visit(context, this);
            from = (Comparable) context.getResult();
        }
        if (toExpr != null) {
            toExpr.visit(context, this);
            to = (Comparable) context.getResult();
        }
        Object result = rangeEvaluator.evaluate(context, node, from, to);
        context.setResult(result);
    }


    private void setHHMMSSss(EasyElRuntimeContext context,
                             Calendar calendar,
                             ASTExpression hoursExpr,
                             ASTExpression minutesExpr,
                             ASTExpression secondsExpr,
                             ASTExpression millisecondsExpr) {
        Integer hours, minutes, seconds, milliseconds;

        if (hoursExpr != null && minutesExpr != null) {
            hoursExpr.visit(context, this);
            hours = (Integer) MetaHelper.unwrap(context.getResult());
            minutesExpr.visit(context, this);
            minutes = (Integer) MetaHelper.unwrap(context.getResult());

            calendar.set(Calendar.HOUR_OF_DAY, hours);
            calendar.set(Calendar.MINUTE, minutes);
        } else {
            calendar.set(Calendar.HOUR_OF_DAY, 0);
            calendar.set(Calendar.MINUTE, 0);
        }

        if (secondsExpr != null) {
            secondsExpr.visit(context, this);
            seconds = (Integer) MetaHelper.unwrap(context.getResult());
            calendar.set(Calendar.SECOND, seconds);
        } else {
            calendar.set(Calendar.SECOND, 0);
        }

        if (millisecondsExpr != null) {
            millisecondsExpr.visit(context, this);
            milliseconds = (Integer) MetaHelper.unwrap(context.getResult());
            calendar.set(Calendar.MILLISECOND, milliseconds);
        } else {
            calendar.set(Calendar.MILLISECOND, 0);
        }

    }

    @Override
    public void visitDate(EasyElRuntimeContext context, ASTDate node) {
        Integer year, month, day;
        Calendar calendar = Calendar.getInstance();

        ASTExpression yearExpr = node.getYear();
        ASTExpression monthExpr = node.getMonth();
        ASTExpression dayExpr = node.getDay();

        ASTExpression hoursExpr = node.getHours();
        ASTExpression minutesExpr = node.getMinutes();
        ASTExpression secondsExpr = node.getSeconds();
        ASTExpression millisecondsExpr = node.getMilliseconds();

        yearExpr.visit(context, this);
        year = (Integer) MetaHelper.unwrap(context.getResult());
        monthExpr.visit(context, this);
        month = (Integer) MetaHelper.unwrap(context.getResult());
        dayExpr.visit(context, this);
        day = (Integer) MetaHelper.unwrap(context.getResult());

        calendar.set(Calendar.YEAR, year);
        calendar.set(Calendar.MONTH, month - 1);
        calendar.set(Calendar.DAY_OF_MONTH, day);

        setHHMMSSss(context, calendar, hoursExpr, minutesExpr, secondsExpr, millisecondsExpr);

        Date time = calendar.getTime();
        EasyElDate date = new EasyElDate(time);
        context.setResult(date);
    }

    @Override
    public void visitTimeDuration(EasyElRuntimeContext context, ASTTimeDuration node) {
        ASTExpression hoursExpr = node.getHours();
        ASTExpression minutesExpr = node.getMinutes();
        ASTExpression secondsExpr = node.getSeconds();
        ASTExpression millisecondsExpr = node.getMilliseconds();

        Integer hours = 0, minutes = 0, seconds = 0, milliseconds = 0;

        if (hoursExpr != null && minutesExpr != null) {
            hoursExpr.visit(context, this);
            hours = (Integer) MetaHelper.unwrap(context.getResult());
            minutesExpr.visit(context, this);
            minutes = (Integer) MetaHelper.unwrap(context.getResult());
        }

        if (secondsExpr != null) {
            secondsExpr.visit(context, this);
            seconds = (Integer) MetaHelper.unwrap(context.getResult());
        }

        if (millisecondsExpr != null) {
            millisecondsExpr.visit(context, this);
            milliseconds = (Integer) MetaHelper.unwrap(context.getResult());
        }

        EasyElTimeDuration timeDuration = new EasyElTimeDuration(0, hours, minutes, seconds, milliseconds);
        context.setResult(timeDuration);
    }

    @Override
    public void visitList(EasyElRuntimeContext context, ASTList node) {
        List<ASTExpression> items = node.getListItems();
        List<Object> list = new ArrayList<>();
        for (ASTExpression item : items) {
            item.visit(context, this);
            list.add(MetaHelper.wrap(context.getResult()));
        }
        context.setResult(MetaHelper.wrap(list));
    }

    @Override
    public void visitListGenerator(EasyElRuntimeContext context, ASTListGenerator node) {
        EasyElRuntimeContext forScopeContext = new EasyElRuntimeContext();
        Map<String, Object> env = new HashMap<>();
        forScopeContext.setParent(context);
        forScopeContext.setEnv(env);
        node.getForCondition().visit(forScopeContext, this);
        Object cond = MetaHelper.unwrap(forScopeContext.getResult());
        List newList = new ArrayList();
        List<ASTIdentifier> nameList = node.getNameList();
        ASTIdentifier firstId = nameList.get(0);
        ASTExpression itemExpr = node.getItem();
        int nameSize = nameList.size();
        if (cond instanceof Iterable) {
            Iterator it = ((Iterable) cond).iterator();
            while (it.hasNext()) {
                Object item = MetaHelper.unwrap(it.next());
                if (nameSize == 1) {
                    env.put(firstId.getName(), MetaHelper.wrap(item));
                } else if (item instanceof Iterable) {
                    Iterator itemIt = ((Iterable) item).iterator();
                    Iterator<ASTIdentifier> nameIt = nameList.iterator();
                    while (itemIt.hasNext() && nameIt.hasNext()) {
                        String name = nameIt.next().getName();
                        Object val = MetaHelper.wrap(itemIt.next());
                        env.put(name, val);
                    }
                    if (nameIt.hasNext()) {
                        throw new EasyElEvalException(context, nameList, "not enough values to assign");
                    }
                    else if (itemIt.hasNext()) {
                        throw new EasyElEvalException(context, nameList, "too many values to assign");
                    }
                } else {
                    throw new EasyElEvalException(context, node,
                            "this '" + item.getClass().getName() + "' item is not a Iterable object");
                }
                itemExpr.visit(forScopeContext, this);
                newList.add(forScopeContext.getResult());
            }
        } else if (cond instanceof Map) {
            Iterator<Map.Entry> it = ((Map) cond).entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry entry = it.next();
                choose: switch (nameSize) {
                    case 1:
                        env.put(firstId.getName(), MetaHelper.wrap(entry.getValue()));
                        break choose;
                    case 2:
                        env.put(firstId.getName(), MetaHelper.wrap(entry.getKey()));
                        env.put(nameList.get(1).getName(), MetaHelper.wrap(entry.getValue()));
                        break choose;
                    default:
                        throw new EasyElEvalException(context, nameList, "not enough values to assign");

                }
                itemExpr.visit(forScopeContext, this);
                newList.add(forScopeContext.getResult());
            }
        } else if (cond instanceof CharSequence) {
            CharSequence str = (CharSequence) cond;
            int len = str.length();
            if (nameSize > 1) {
                throw new EasyElEvalException(context, nameList, "not enough values to assign");
            }
            for (int i = 0; i < len; i++) {
                char ch = str.charAt(i);
                env.put(firstId.getName(), MetaHelper.wrap(ch));
                itemExpr.visit(forScopeContext, this);
                newList.add(forScopeContext.getResult());
            }
        } else {
            throw new EasyElEvalException(context, node.getForCondition(),
                    "list generator condition expression do not support for type '" + cond.getClass().getName() + "'");
        }
        context.setResult(MetaHelper.wrap(newList));
    }

    @Override
    public void visitMap(EasyElRuntimeContext context, ASTMap node) {
        List<ASTMapEntry> entries = node.getEntryList();
        Map map = new HashMap();
        for (ASTMapEntry entry : entries) {
            entry.visit(context, this);
            Object[] pair = (Object[]) context.getResult();
            Object key = pair[0];
            Object value = pair[1];
            map.put(key, value);
        }
        context.setResult(map);
    }

    @Override
    public void visitMapEntry(EasyElRuntimeContext context, ASTMapEntry node) {
        ASTExpression keyExpr = node.getKey();
        ASTExpression valueExpr = node.getValue();

        Object key = keyExpr.getText();
        valueExpr.visit(context, this);
        Object value = MetaHelper.unwrap(context.getResult());

        Object[] keyValuePair = new Object[] {key, value};
        context.setResult(keyValuePair);
    }

    @Override
    public void visitNull(EasyElRuntimeContext context, ASTNull node) {
        context.setResult(null);
    }

    @Override
    public void visitNewInstance(EasyElRuntimeContext context, ASTNewInstance node) {
        List<ASTExpression> argExprs = node.getArguments();
        Object[] args = getArgumentsArray(context, argExprs);
        Object instance = newInstanceEvaluator.evaluate(context, node, args);
        ASTMap properties = node.getProperties();
        if (properties != null) {
            List<ASTMapEntry> propMapEntries = properties.getEntryList();
            for (ASTMapEntry propEntry : propMapEntries) {
                propEntry.visit(context, this);
                Object[] pair = (Object[]) context.getResult();
                String propName = (String) pair[0];
                Object value = pair[1];
                try {
                    AccessUtils.injectObject(instance, propName, value);
                } catch (IllegalAccessException e) {
                    throw new EasyElEvalException(context, node, e.getMessage(), e);
                } catch (InvocationTargetException e) {
                    throw new EasyElEvalException(context, node, e.getMessage(), e);
                } catch (EasyElAccessException e) {
                    throw new EasyElEvalException(context, node, e.getMessage(), e);
                }
            }
        }
        context.setResult(instance);
    }

    @Override
    public void visitIdentifier(EasyElRuntimeContext context, ASTIdentifier node) {
        Object ret = identifierEvaluator.evaluate(context, node);
        context.setResult(ret);
    }


    @Override
    public void visitBinaryExpression(EasyElRuntimeContext context, ASTBinaryExpression node) {
        ASTExpression leftExpr = node.getLeft();
        ASTExpression rightExpr = node.getRight();
        leftExpr.visit(context, this);
        Object left = context.getResult();
        rightExpr.visit(context, this);
        Object right = context.getResult();

        Token op = node.getOperation();
        int opType = op.getType();
        Object ret = null;
        switch (opType) {
            case EasyElParser.ADD:
                ret = addEvaluator.evaluate(context, node, left, right);
                break;
            case EasyElParser.SUB:
                ret = subEvaluator.evaluate(context, node, left, right);
                break;
            case EasyElParser.MUL:
                ret = multiplyEvaluator.evaluate(context, node, left, right);
                break;
            case EasyElParser.DIV:
                ret = divideEvaluator.evaluate(context, node, left, right);
                break;
            case EasyElParser.MOD:
                ret = remainderEvaluator.evaluate(context, node, left, right);
                break;
            case EasyElParser.POWER:
                ret = powerEvaluator.evaluate(context, node, left, right);
                break;
            case EasyElParser.AND:
            case EasyElParser.J_AND:
            case EasyElParser.OR:
            case EasyElParser.J_OR:
                ret = booleanEvaluator.evaluate(context, node, left, right);
                break;
            case EasyElParser.GT:
            case EasyElParser.GE:
            case EasyElParser.LT:
            case EasyElParser.LE:
            case EasyElParser.EQ:
            case EasyElParser.NOT_EQ:
                ret = compareEvaluator.evaluate(context, node, left, right);
                break;
            case EasyElParser.REGEX_MATCH:
                ret = regexMatchEvaluator.evaluate(context, node, left, right);
                break;
            case EasyElParser.NOT_REGEX_MATCH:
                ret = !regexMatchEvaluator.evaluate(context, node, left, right);
                break;
            case EasyElParser.IN:
                ret = inEvaluator.evaluate(context, node, left, right);
                break;
            case EasyElParser.NOT_IN:
                ret = !inEvaluator.evaluate(context, node, left, right);
                break;
        }
        context.setResult(ret);
    }

    @Override
    public void visitTernaryExpression(EasyElRuntimeContext context, ASTTernaryExpression node) {
        node.getCondition().visit(context, this);
        Object conRet = MetaHelper.unwrap(context.getResult());
        if (conRet == null || Boolean.FALSE.equals(conRet)) {
            // when condition is false
            if (node.getFalseExpression() != null) {
                node.getFalseExpression().visit(context, this);
            } else {
                context.setResult(null);
            }
        } else {
            // when condition is true
            if (node.getTrueExpression() != null) {
                node.getTrueExpression().visit(context, this);
            } else {
                context.setResult(context.getResult());
            }
        }
    }

    @Override
    public void visitNegativeExpression(EasyElRuntimeContext context, ASTNegativeExpression node) {
        node.getExpression().visit(context, this);
        Object right = context.getResult();
        try {
            NumberWrapper numberWrapper = (NumberWrapper) right;
            NumberWrapper ret = numberWrapper.negate();
            context.setResult(ret);
        } catch (Throwable th){
            throw new EasyElEvalException(context, node,
                    "Operator '-' can not applied to " + right.getClass().getName());
        }
    }

    @Override
    public void visitNotExpression(EasyElRuntimeContext context, ASTNotExpression node) {
        node.getExpression().visit(context, this);
        Object ret = notEvaluator.evaluate(context, node, context.getResult());
        context.setResult(ret);
    }

    @Override
    public void visitAccessExpression(EasyElRuntimeContext context, ASTAccessExpression node) {
        if (node.isStatic()) {
            Object ret = accessEvaluator.evaluate(context, node, node.getLeft());
            context.setResult(ret);
        } else {
            node.getLeft().visit(context, this);
            Object left = context.getResult();
            Object ret = accessEvaluator.evaluate(context, node, left);
            context.setResult(ret);
        }
    }

    @Override
    public void visitIndexExpression(EasyElRuntimeContext context, ASTIndexExpression node) {
        node.getLeft().visit(context, this);
        Object left = context.getResult();
        node.getIndex().visit(context, this);
        Object index = context.getResult();
        Object ret = indexEvaluator.evaluate(context, node, left, index);
        context.setResult(ret);
    }

    @Override
    public void visitInvokeExpression(EasyElRuntimeContext context, ASTInvokeExpression node) {
        ASTExpression leftExpr = node.getLeft();
        Object left = null;
        if (leftExpr != null) {
            leftExpr.visit(context, this);
            left = context.getResult();
        }
        List<ASTExpression> argExprs = node.getArguments();
        Object[] args = getArgumentsArray(context, argExprs);
        Object ret = invokeEvaluator.evaluate(context, node, left, args);
        context.setResult(ret);
    }


    private Object[] getArgumentsArray(EasyElRuntimeContext context, List<ASTExpression> argExprs) {
        Object[] args = new Object[argExprs.size()];
        for (int i = 0; i < argExprs.size(); i++) {
            ASTExpression argExpr = argExprs.get(i);
            argExpr.visit(context, this);
            args[i] = context.getResult();
        }
        return args;
    }

}
